1. 파일 오픈과 충돌 방지 락 (File Lock)

  • 파일을 열었을 때 OS 차원에서 다른 프로세스와의 동시 수정이나 삭제를 차단하기 위해 기본적으로 파일 락(Lock)이 설정됩니다.

  • 특히 write 모드로 열었을 경우, 여러 프로세스가 동시에 같은 위치에 데이터를 기록하여 파일이 깨지는 현상(충돌)을 방지합니다.

  • OS별 특징:

    • Windows: fopen으로 쓰기 모드 오픈 시 OS 레벨에서 강제 락(Mandatory Lock)이 걸려 외부에서의 수정/삭제가 차단됩니다.
    • Linux/POSIX: 기본적으로 권고 락(Advisory Lock) 정책을 따르므로, 완벽한 프로세스 간 동기화가 필요할 때는 flock() 또는 fcntl() 시스템 콜을 병행합니다.

2. 파일 오픈 모드별 핵심 동작과 주의사항

  • "w" / "wb" (가장 위험한 모드):

    • 기존 파일이 존재하면 파일 크기를 0바이트로 초기화(Truncate)한 후 새로 씁니다.
    • 기존 데이터가 전부 지워지므로 영구적인 데이터 유실을 주의해야 합니다.
  • "a" / "ab" (추가/누적 모드):

    • 기존 내용을 보존하며, 파일 포인터의 위치와 상관없이 항상 파일 끝(EOF)에 데이터를 덧붙입니다.
    • 로그 파일이나 데이터를 누적 기록할 때는 반드시 a 모드를 사용해야 합니다.
  • "r+" / "rb+" (읽기/쓰기 겸용 모드):

    • 기존 내용은 보존되지만, 파일을 열었을 때 파일 포인터가 맨 처음(0)에 위치합니다.
    • 별도로 오프셋을 이동하지 않고 쓰기를 실행하면 첫 시작 부분부터 기존 데이터를 덮어쓰기(Override)하므로 기존 데이터가 손상될 위험이 큽니다.
  • "r" / "rb" (읽기 전용 모드):

    • 파일이 없으면 NULL을 반환하며, 시작 오프셋 0부터 순차적으로 읽습니다.

3. 텍스트(Text) 모드 vs 바이너리(Binary) 모드

  • 텍스트 모드 ("r", "w", "a")

    • 사람이 읽을 수 있는 문자열 데이터를 읽고 쓸 때 사용합니다.
    • 줄바꿈 자동 변환: OS 환경에 따라 개행 문자 변환이 자동으로 일어납니다. (Windows: \n \r\n)
  • 바이너리 모드 ("rb", "wb", "ab")

    • JSON 형태의 바이트 데이터struct 구조체 규격에 맞춰 메모리 형태 그대로(Raw Bytes) 1:1로 정해진 길이만큼 읽고 쓸 때 사용합니다.
    • 개행 문자 자동 변환이 일어나지 않아 바이너리 데이터가 변형되지 않습니다.

4. 바이너리 입출력 시 필수 검증 (Validation)

바이너리 I/O는 메모리 공간을 직접 다루므로 엄격한 유효성 검증을 거치지 않으면 버퍼 오버플로우 및 메모리 오염(Memory Corruption)이 발생합니다.

#include <stdio.h>
#include <stdint.h>

#pragma pack(push, 1) // 패딩 제거로 파일 규격 1:1 일치
typedef struct _PacketData {
    uint8_t  cmd;
    uint32_t length;
    char     payload[256];
} PacketData;
#pragma pack(pop)

void process_binary_file(FILE *fp) {
    PacketData packet = {0};

    // 1. 읽어들인 블록 수 검증 (데이터 잘림 및 파일 손상 체크)
    size_t read_count = fread(&packet, sizeof(PacketData), 1, fp);
    if (read_count != 1) {
        fprintf(stderr, "[오류] 파일 크기 부족 또는 바이너리 규격 불일치\n");
        return;
    }

    // 2. 버퍼 오버플로우 방지: 페이로드 길이 검증
    if (packet.length >= sizeof(packet.payload)) {
        fprintf(stderr, "[오류] 비정상적인 페이로드 길이 (오버플로우 위험)\n");
        return;
    }

    // 3. 메모리 침범 방지: 문자열 끝 NULL 종단 강제 보장
    packet.payload[sizeof(packet.payload) - 1] = '\0';
}

주의 (구조체 바이너리 저장 시 포인터 사용 금지):

구조체 내부에 char *, int * 등 포인터 변수가 포함되어 있을 경우, fwrite 시 실 데이터가 아닌 ‘임의의 메모리 주소값’만 파일에 기록됩니다. 파일을 다시 읽었을 때 해당 주소는 허상 포인터(Dangling Pointer)가 되어 접근 시 즉시 크래시(Segmentation Fault)를 일으킵니다.